// Module for interfacing with file system

#include "module.h"
#include "lauxlib.h"
#include "lmem.h"
#include "platform.h"
#include "spiffs/nodemcu_spiffs.h"

#include <stdint.h>
#include "vfs.h"
#include <string.h>

#include <alloca.h>

#define FILE_READ_CHUNK 1024

// use this time/date in absence of a timestamp
#define FILE_TIMEDEF_YEAR 1970
#define FILE_TIMEDEF_MON 01
#define FILE_TIMEDEF_DAY 01
#define FILE_TIMEDEF_HOUR 00
#define FILE_TIMEDEF_MIN 00
#define FILE_TIMEDEF_SEC 00

static int file_fd = 0;
static int file_fd_ref = LUA_NOREF;
static int rtc_cb_ref = LUA_NOREF;

typedef struct _file_fd_ud {
  int fd;
} file_fd_ud;

static void do_flash_mount() {
    if (!vfs_mount("/FLASH", 0)) {
        // Failed to mount -- try reformat
        dbg_printf("Formatting file system. Please wait...\n");
        if (!vfs_format()) {
            NODE_ERR( "\n*** ERROR ***: unable to format. FS might be compromised.\n" );
            NODE_ERR( "It is advised to re-flash the NodeMCU image.\n" );
        }
    }
}

static void table2tm( lua_State *L, vfs_time *tm )
{
  int idx = lua_gettop( L );

  // extract items from table
  lua_getfield( L, idx, "year" );
  lua_getfield( L, idx, "mon" );
  lua_getfield( L, idx, "day" );
  lua_getfield( L, idx, "hour" );
  lua_getfield( L, idx, "min" );
  lua_getfield( L, idx, "sec" );

  tm->year = luaL_optint( L, ++idx, FILE_TIMEDEF_YEAR );
  tm->mon  = luaL_optint( L, ++idx, FILE_TIMEDEF_MON );
  tm->day  = luaL_optint( L, ++idx, FILE_TIMEDEF_DAY );
  tm->hour = luaL_optint( L, ++idx, FILE_TIMEDEF_HOUR );
  tm->min  = luaL_optint( L, ++idx, FILE_TIMEDEF_MIN );
  tm->sec  = luaL_optint( L, ++idx, FILE_TIMEDEF_SEC );

  // remove items from stack
  lua_pop( L, 6 );
}

static sint32_t file_rtc_cb( vfs_time *tm )
{
  sint32_t res = VFS_RES_ERR;

  if (rtc_cb_ref != LUA_NOREF) {
    lua_State *L = lua_getstate();

    lua_rawgeti( L, LUA_REGISTRYINDEX, rtc_cb_ref );
    if (luaL_pcallx( L, 0, 1 ) != LUA_OK)
      return res;

    if (lua_type( L, lua_gettop( L ) ) == LUA_TTABLE) {
      table2tm( L, tm );
      res = VFS_RES_OK;
    }

    // pop item returned by callback
    lua_pop( L, 1 );
  }

  return res;
}

// Lua: on()
static int file_on(lua_State *L)
{
  enum events{
    ON_RTC = 0
  };
  const char *const eventnames[] = {"rtc", NULL};

  int event = luaL_checkoption(L, 1, "rtc", eventnames);

  switch (event) {
  case ON_RTC:
    luaL_unref(L, LUA_REGISTRYINDEX, rtc_cb_ref);

    switch(lua_type(L, 2)) {
    case LUA_TFUNCTION:
      lua_pushvalue(L, 2);  // copy argument (func) to the top of stack
      rtc_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
      vfs_register_rtc_cb(file_rtc_cb);
      break;
    case LUA_TNIL:
      rtc_cb_ref = LUA_NOREF;
      vfs_register_rtc_cb(NULL);
      break;
    default:
      luaL_error(L, "Callback should be function or nil");
    }

    break;
  default:
    break;
  }

  return 0;
}

// Lua: close()
static int file_close( lua_State* L )
{
  int need_pop = FALSE;
  file_fd_ud *ud;

  if (lua_type( L, 1 ) != LUA_TUSERDATA) {
    // fall back to last opened file
    if (file_fd_ref != LUA_NOREF) {
      lua_rawgeti( L, LUA_REGISTRYINDEX, file_fd_ref );
      // top of stack is now default file descriptor
      ud = (file_fd_ud *)luaL_checkudata(L, -1, "file.obj");
      lua_pop( L, 1 );
    } else {
      // no default file currently opened
      return 0;
    }
  } else {
    ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
  }

  if(ud->fd){
      vfs_close(ud->fd);
      // mark as closed
      ud->fd = 0;
  }

  // unref default file descriptor
  luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
  file_fd_ref = LUA_NOREF;

  return 0;
}

static int file_obj_free( lua_State *L )
{
  file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
  if (ud->fd) {
    // close file if it's still open
    vfs_close(ud->fd);
    ud->fd = 0;
  }

  return 0;
}

// Lua: format()
static int file_format( lua_State* L )
{
  size_t len;
  file_close(L);
  if( !vfs_format() )
  {
    NODE_ERR( "\n*** ERROR ***: unable to format. FS might be compromised.\n" );
    NODE_ERR( "It is advised to re-flash the NodeMCU image.\n" );
    luaL_error(L, "Failed to format file system");
  }
  else{
    NODE_ERR( "format done.\n" );
  }
  return 0;
}

static int file_fscfg (lua_State *L)
{
  uint32_t phys_addr, phys_size;

  vfs_fscfg("/FLASH", &phys_addr, &phys_size);

  lua_pushinteger (L, phys_addr);
  lua_pushinteger (L, phys_size);
  return 2;
}

// Lua: open(filename, mode)
static int file_open( lua_State* L )
{
  size_t len;

  // unref last file descriptor to allow gc'ing if not kept by user script
  luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
  file_fd_ref = LUA_NOREF;

  const char *fname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");

  const char *mode = luaL_optstring(L, 2, "r");

  file_fd = vfs_open(fname, mode);

  if(!file_fd){
    lua_pushnil(L);
  } else {
    file_fd_ud *ud = (file_fd_ud *) lua_newuserdata( L, sizeof( file_fd_ud ) );
    ud->fd = file_fd;
    luaL_getmetatable( L, "file.obj" );
    lua_setmetatable( L, -2 );

    // store reference to opened file
    lua_pushvalue( L, -1 );
    file_fd_ref = luaL_ref( L, LUA_REGISTRYINDEX );
  }
  return 1;
}

// Lua: list()
static int file_list( lua_State* L )
{
  vfs_dir  *dir;
  const char *pattern;
  struct vfs_stat stat;
  int pcres;

  lua_settop(L, 1);
  pattern = luaL_optstring(L, 1, NULL);   /* Pattern (arg) or nil (not) at 1 */

  dir = vfs_opendir("");
  if (dir == NULL) {
    return 0;
  }

  lua_newtable( L );                      /* Table at 2 */

  if (pattern) {
    /*
     * We know that pattern is a string, and so the "match" method will always
     * exist.  No need to check return value here
     */
    luaL_getmetafield( L, 1, "match" );  /* Function at 3 */
  }

  while (vfs_readdir(dir, &stat) == VFS_RES_OK) {
    if (pattern) {
      lua_settop( L, 3 );                 /* Ensure nothing else on stack */

      /* Construct and pcall(string.match,name,pattern) */
      lua_pushvalue( L, 3 );
      lua_pushstring( L, stat.name );
      lua_pushvalue( L, 1 );
      pcres = lua_pcall( L, 2, 1, 0 );
      if (pcres != 0) {
        vfs_closedir(dir);
        lua_error( L );
      }
      if (lua_isnil( L, -1 )) {
        continue;
      }
    }
    lua_pushinteger( L, stat.size );
    lua_setfield( L, 2, stat.name );
  }

  /* Shed everything back to Table */
  lua_settop( L, 2 );
  vfs_closedir(dir);
  return 1;
}

static int get_file_obj( lua_State *L, int *argpos )
{
  if (lua_type( L, 1 ) == LUA_TUSERDATA) {
    file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
    *argpos = 2;
    return ud->fd;
  } else {
    *argpos = 1;
    return file_fd;
  }
}

#define GET_FILE_OBJ int argpos; \
  int fd = get_file_obj( L, &argpos );

static int file_seek (lua_State *L)
{
  GET_FILE_OBJ;

  static const int mode[] = {VFS_SEEK_SET, VFS_SEEK_CUR, VFS_SEEK_END};
  static const char *const modenames[] = {"set", "cur", "end", NULL};
  if(!fd)
    return luaL_error(L, "open a file first");
  int op = luaL_checkoption(L, argpos, "cur", modenames);
  long offset = luaL_optlong(L, ++argpos, 0);
  op = vfs_lseek(fd, offset, mode[op]);
  if (op < 0)
    lua_pushnil(L);  /* error */
  else
    lua_pushinteger(L, vfs_tell(fd));
  return 1;
}

// Lua: exists(filename)
static int file_exists( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");

  struct vfs_stat stat;
  lua_pushboolean(L, vfs_stat((char *)fname, &stat) == VFS_RES_OK ? 1 : 0);

  return 1;
}

// Lua: remove(filename)
static int file_remove( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( fname );
  luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");
  vfs_remove((char *)fname);
  return 0;
}

// Lua: flush()
static int file_flush( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  if(vfs_flush(fd) == 0)
    lua_pushboolean(L, 1);
  else
    lua_pushnil(L);
  return 1;
}

// Lua: rename("oldname", "newname")
static int file_rename( lua_State* L )
{
  size_t len;

  const char *oldname = luaL_checklstring( L, 1, &len );
  const char *basename = vfs_basename( oldname );
  luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(oldname) == len, 1, "filename invalid");

  const char *newname = luaL_checklstring( L, 2, &len );
  basename = vfs_basename( newname );
  luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(newname) == len, 2, "filename invalid");

  if(0 <= vfs_rename( oldname, newname )){
    lua_pushboolean(L, 1);
  } else {
    lua_pushboolean(L, 0);
  }
  return 1;
}

// Lua: stat(filename)
static int file_stat( lua_State* L )
{
  size_t len;
  const char *fname = luaL_checklstring( L, 1, &len );
  luaL_argcheck( L, strlen(fname) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid" );

  struct vfs_stat stat;
  if (vfs_stat( (char *)fname, &stat ) != VFS_RES_OK) {
    lua_pushnil( L );
    return 1;
  }

  lua_createtable( L, 0, 7 );

  lua_pushinteger( L, stat.size );
  lua_setfield( L, -2, "size" );

  lua_pushstring( L, stat.name );
  lua_setfield( L, -2, "name" );

  lua_pushboolean( L, stat.is_dir );
  lua_setfield( L, -2, "is_dir" );

  lua_pushboolean( L, stat.is_rdonly );
  lua_setfield( L, -2, "is_rdonly" );

  lua_pushboolean( L, stat.is_hidden );
  lua_setfield( L, -2, "is_hidden" );

  lua_pushboolean( L, stat.is_sys );
  lua_setfield( L, -2, "is_sys" );

  lua_pushboolean( L, stat.is_arch );
  lua_setfield( L, -2, "is_arch" );

  // time stamp as sub-table
  lua_createtable( L, 0, 6 );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.year : FILE_TIMEDEF_YEAR );
  lua_setfield( L, -2, "year" );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.mon : FILE_TIMEDEF_MON );
  lua_setfield( L, -2, "mon" );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.day : FILE_TIMEDEF_DAY );
  lua_setfield( L, -2, "day" );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.hour : FILE_TIMEDEF_HOUR );
  lua_setfield( L, -2, "hour" );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.min : FILE_TIMEDEF_MIN );
  lua_setfield( L, -2, "min" );

  lua_pushinteger( L, stat.tm_valid ? stat.tm.sec : FILE_TIMEDEF_SEC );
  lua_setfield( L, -2, "sec" );

  lua_setfield( L, -2, "time" );

  return 1;
}

// g_read()
static int file_g_read( lua_State* L, int n, int16_t end_char, int fd )
{
  int i, j;
  luaL_Buffer b;
  char p[LUAL_BUFFERSIZE/2];

  if(!fd)
    return luaL_error(L, "open a file first");

  luaL_buffinit(L, &b);

  for (j = 0; j < n; j += sizeof(p)) {
    int nwanted = (n - j >= sizeof(p)) ? sizeof(p) : n - j;
    int nread   = vfs_read(fd, p, nwanted);

    if (nread == VFS_RES_ERR || nread == 0) {
      if (j > 0) {
        break;
      }
      lua_pushnil(L);
      return 1;
    }

    for (i = 0; i < nread; ++i) {
      luaL_addchar(&b, p[i]);
      if (p[i] == end_char) {
        vfs_lseek(fd, -nread + i + 1, VFS_SEEK_CUR); //reposition after end char found
        nread = 0;   // force break on outer loop
        break;
      }
    }

    if (nread < nwanted)
      break;
  }
  luaL_pushresult(&b);
  return 1;
}

// Lua: read()
// file.read() will read FILE _CHUNK bytes, or EOF is reached.
// file.read(10) will read 10 byte from file, or EOF is reached.
// file.read('q') will read until 'q' or EOF is reached.
static int file_read( lua_State* L )
{
  unsigned need_len = FILE_READ_CHUNK;
  int16_t end_char = EOF;
  size_t el;

  GET_FILE_OBJ;

  if( lua_type( L, argpos ) == LUA_TNUMBER )
  {
    need_len = ( unsigned )luaL_checkinteger( L, argpos );
  }
  else if(lua_isstring(L, argpos))
  {
    const char *end = luaL_checklstring( L, argpos, &el );
    if(el!=1){
      return luaL_error( L, "wrong arg range" );
    }
    end_char = (int16_t)end[0];
  }

  return file_g_read(L, need_len, end_char, fd);
}

// Lua: readline()
static int file_readline( lua_State* L )
{
  GET_FILE_OBJ;

  return file_g_read(L, FILE_READ_CHUNK, '\n', fd);
}

// Lua: getfile(filename)
static int file_getfile( lua_State* L )
{
  // Warning this code C calls other file_* routines to avoid duplication code.  These
  // use Lua stack addressing of arguments, so this does Lua stack maniplation to
  // align these
  int ret_cnt = 0;
  lua_settop(L ,1);
  // Stack [1] = FD
  file_open(L);
  // Stack [1] = filename; [2] = FD or nil
  if (!lua_isnil(L, -1)) {
    lua_remove(L, 1);  // dump filename, so [1] = FD
    file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
    ret_cnt = file_g_read(L, LUAI_MAXINT32, EOF, ud->fd);
    // Stack [1] = FD; [2] = contents if ret_cnt = 1;
    file_close(L);     // leaves Stack unchanged if [1] = FD
    lua_remove(L, 1);  // Dump FD leaving contents as [1] / ToS
  }
  return ret_cnt;
}

// Lua: write("string")
static int file_write( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  size_t l, rl;
  const char *s = luaL_checklstring(L, argpos, &l);
  rl = vfs_write(fd, s, l);
  if(rl==l)
    lua_pushboolean(L, 1);
  else
    lua_pushnil(L);
  return 1;
}

// Lua: writeline("string")
static int file_writeline( lua_State* L )
{
  GET_FILE_OBJ;

  if(!fd)
    return luaL_error(L, "open a file first");
  size_t l, rl;
  const char *s = luaL_checklstring(L, argpos, &l);
  rl = vfs_write(fd, s, l);
  if(rl==l){
    rl = vfs_write(fd, "\n", 1);
    if(rl==1)
      lua_pushboolean(L, 1);
    else
      lua_pushnil(L);
  }
  else{
    lua_pushnil(L);
  }
  return 1;
}

// Lua: getfile(filename)
static int file_putfile( lua_State* L )
{
  // Warning this code C calls other file_* routines to avoid duplication code.  These
  // use Lua stack addressing of arguments, so this does Lua stack maniplation to
  // align these
  int ret_cnt = 0;
  lua_settop(L, 2);
  lua_pushvalue(L, 2); //dup contents onto the ToS [3]
  lua_pushliteral(L, "w+");
  lua_replace(L, 2);
  // Stack [1] = filename; [2] "w+" [3] contents;
  file_open(L);
  // Stack [1] = filename; [2] "w+" [3] contents; [4] FD or nil

  if (!lua_isnil(L, -1)) {
    lua_remove(L, 2);  //dump "w+" attribute literal
    lua_replace(L, 1);
    // Stack [1] = FD; [2] contents
    file_write(L);
    // Stack [1] = FD; [2] contents; [3] result status
    lua_remove(L, 2);  //dump contents
    file_close(L);
    lua_remove(L, 1); // Dump FD leaving status as ToS
  }
  return 1;
}

// Lua: fsinfo()
static int file_fsinfo( lua_State* L )
{
  uint32_t total, used;
  if (vfs_fsinfo("", &total, &used)) {
    return luaL_error(L, "file system failed");
  }
  NODE_DBG("total: %d, used:%d\n", total, used);
  if(total>0x7FFFFFFF || used>0x7FFFFFFF || used > total)
  {
    return luaL_error(L, "file system error");
  }
  lua_pushinteger(L, total-used);
  lua_pushinteger(L, used);
  lua_pushinteger(L, total);
  return 3;
}

typedef struct {
  vfs_vol *vol;
} volume_type;

// Lua: vol = file.mount("/SD0")
static int file_mount( lua_State *L )
{
  const char *ldrv = luaL_checkstring( L, 1 );
  int num = luaL_optint( L, 2, -1 );
  volume_type *vol = (volume_type *)lua_newuserdata( L, sizeof( volume_type ) );

  if (vol->vol = vfs_mount( ldrv, num )) {
    /* set its metatable */
    luaL_getmetatable(L, "file.vol");
    lua_setmetatable(L, -2);
    return 1;
  } else {
    // remove created userdata
    lua_pop( L, 1 );
    return 0;
  }
}

// Lua: success = file.chdir("/SD0/")
static int file_chdir( lua_State *L )
{
  const char *path = luaL_checkstring( L, 1 );

  lua_pushboolean( L, 0 <= vfs_chdir( path ) );
  return 1;
}

static int file_vol_umount( lua_State *L )
{
  volume_type *vol = luaL_checkudata( L, 1, "file.vol" );
  luaL_argcheck( L, vol, 1, "volume expected" );

  lua_pushboolean( L, 0 <= vfs_umount( vol->vol ) );

  // invalidate vfs descriptor, it has been free'd anyway
  vol->vol = NULL;
  return 1;
}


LROT_BEGIN(file_obj, NULL, LROT_MASK_GC_INDEX)
  LROT_FUNCENTRY( __gc, file_obj_free )
  LROT_TABENTRY(  __index, file_obj )
  LROT_FUNCENTRY( close, file_close )
  LROT_FUNCENTRY( read, file_read )
  LROT_FUNCENTRY( readline, file_readline )
  LROT_FUNCENTRY( write, file_write )
  LROT_FUNCENTRY( writeline, file_writeline )
  LROT_FUNCENTRY( seek, file_seek )
  LROT_FUNCENTRY( flush, file_flush )
LROT_END(file_obj, NULL, LROT_MASK_GC_INDEX)


LROT_BEGIN(file_vol, NULL, LROT_MASK_INDEX)
  LROT_TABENTRY( __index, file_vol )
  LROT_FUNCENTRY( umount, file_vol_umount )
LROT_END(file_vol, NULL, LROT_MASK_INDEX)

// Module function map
LROT_BEGIN(file, NULL, 0)
  LROT_FUNCENTRY( list, file_list )
  LROT_FUNCENTRY( open, file_open )
  LROT_FUNCENTRY( close, file_close )
  LROT_FUNCENTRY( write, file_write )
  LROT_FUNCENTRY( writeline, file_writeline )
  LROT_FUNCENTRY( read, file_read )
  LROT_FUNCENTRY( readline, file_readline )
#ifdef BUILD_SPIFFS
  LROT_FUNCENTRY( format, file_format )
  LROT_FUNCENTRY( fscfg, file_fscfg )
#endif
  LROT_FUNCENTRY( remove, file_remove )
  LROT_FUNCENTRY( seek, file_seek )
  LROT_FUNCENTRY( flush, file_flush )
  LROT_FUNCENTRY( rename, file_rename )
  LROT_FUNCENTRY( exists, file_exists )
  LROT_FUNCENTRY( getcontents, file_getfile )
  LROT_FUNCENTRY( putcontents, file_putfile )
  LROT_FUNCENTRY( fsinfo, file_fsinfo )
  LROT_FUNCENTRY( on, file_on )
  LROT_FUNCENTRY( stat, file_stat )
#ifdef BUILD_FATFS
  LROT_FUNCENTRY( mount, file_mount )
  LROT_FUNCENTRY( chdir, file_chdir )
#endif
LROT_END(file, NULL, 0)


int luaopen_file( lua_State *L ) {
  int startup_option = platform_rcr_get_startup_option();
  if ((startup_option & STARTUP_OPTION_DELAY_MOUNT) == 0) {
      do_flash_mount();
  } else {
      myspiffs_set_automount(do_flash_mount);
  }
  luaL_rometatable( L, "file.vol",  LROT_TABLEREF(file_vol));
  luaL_rometatable( L, "file.obj",  LROT_TABLEREF(file_obj));
  return 0;
}

NODEMCU_MODULE(FILE, "file", file, luaopen_file);
